package ljl.bilibili.search.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ljl.bilibili.client.pojo.RecommendVideo;
import ljl.bilibili.entity.user_center.user_relationships.Follow;
import ljl.bilibili.mapper.user_center.user_relationships.FollowMapper;
import ljl.bilibili.search.service.SearchService;
import ljl.bilibili.search.vo.request.EsIndexRequest;
import ljl.bilibili.search.vo.request.EsKeywordRequest;
import ljl.bilibili.search.vo.response.TotalCountSearchResponse;
import ljl.bilibili.search.vo.response.UserKeyWordSearchResponse;
import ljl.bilibili.search.vo.response.VideoKeywordSearchResponse;
import ljl.bilibili.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MoreLikeThisQueryBuilder.Item;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;

import static ljl.bilibili.search.constant.Constant.*;

/**
 * 搜索相关
 */
@Service
@Slf4j
public class SearchServiceImpl implements SearchService {
    @Resource
    public RestHighLevelClient client;
    final int size = 20;
    @Resource
    FollowMapper followMapper;
    @Resource
    ObjectMapper objectMapper;

    /**
     * 获取用户、视频总匹配文档数和页数
     */
    @Override
    public Result<TotalCountSearchResponse> totalKeywordSearch(String keyword) {
        // 创建针对视频索引的搜索请求，指定要搜索的索引名称为"video"
        SearchRequest videoSearchRequest = new SearchRequest(VIDEO_INDEX_NAME);
        // 创建用于视频搜索的源构建器，SearchSourceBuilder 提供了灵活的方式来设置各种搜索参数，如查询条件、排序规则等。
        SearchSourceBuilder videoSearchSourceBuilder = new SearchSourceBuilder();
        // 设置视频搜索查询，匹配关键词到多个字段，并指定查询类型为最匹配字段
        videoSearchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, MULTI_QUERY_VIDEO_NAME, MULTI_QUERY_AUTHOR_NAME, MULTI_QUERY_INTRO)
                .type(MultiMatchQueryBuilder.Type.MOST_FIELDS));
        // 设置视频搜索的最低分数阈值，过滤掉评分过低的结果，确保返回的搜索结果具有较高的相关性。
        videoSearchSourceBuilder.minScore(1.0f);
        // 根据分数降序排序视频搜索结果，确保最相关的搜索结果排在前面，提升用户体验。
        videoSearchSourceBuilder.sort(ORDER_BY_SCORE, SortOrder.DESC);
        // 设置视频搜索结果的源信息获取为true
        videoSearchSourceBuilder.fetchSource(true);
        // 将构建好的源应用到视频搜索请求中
        videoSearchRequest.source(videoSearchSourceBuilder);

        // 创建针对用户索引的搜索请求，指定要搜索的索引名称为"user"
        SearchRequest userSearchRequest = new SearchRequest(USER_INDEX_NAME);
        // 创建用于用户搜索的源构建器
        SearchSourceBuilder userSearchSourceBuilder = new SearchSourceBuilder();
        // 设置用户搜索的最低分数阈值
        userSearchSourceBuilder.minScore(1.0f);
        // 设置用户搜索查询，匹配关键词到多个字段，并指定查询类型为最匹配字段
        userSearchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, MULTI_QUERY_NICKNAME, MULTI_QUERY_INTRO)
                .type(MultiMatchQueryBuilder.Type.MOST_FIELDS));
        // 根据分数降序排序用户搜索结果
        userSearchSourceBuilder.sort(ORDER_BY_SCORE, SortOrder.DESC);
        // 设置用户搜索结果的源信息获取为true
        userSearchSourceBuilder.fetchSource(true);
        // 将构建好的源应用到用户搜索请求中
        userSearchRequest.source(userSearchSourceBuilder);

        // 声明两个 SearchResponse 类型的变量，用于存储从 Elasticsearch 返回的搜索结果。
        SearchResponse videoResponse;
        SearchResponse userResponse;
        try {
            videoResponse = client.search(videoSearchRequest, RequestOptions.DEFAULT);
            userResponse = client.search(userSearchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 计算视频搜索的总命中数和总页数
        long totalVideoCount = videoResponse.getHits().getTotalHits().value;
        long totalVideoPages = totalVideoCount / size + (totalVideoCount % size == 0 ? 0 : 1);
        // 计算用户搜索的总命中数和总页数
        long totalUserCount = userResponse.getHits().getTotalHits().value;
        long totalUserPages = totalUserCount / size + (totalUserCount % size == 0 ? 0 : 1);

        // 返回包含总命中数和总页数的响应对象
        return Result.data(new TotalCountSearchResponse().setTotalVideoPage(totalVideoPages).setTotalVideoNum(totalVideoCount)
                .setTotalUserNum(totalUserCount).setTotalUserPage(totalUserPages));
    }

    /**
     * 添加搜索记录
     */
    @Override
    public Result<Boolean> addKeywordSearchRecord(EsKeywordRequest esKeywordRequest) throws IOException {
        //创建索引对象
        IndexRequest indexRequest = new IndexRequest(HISTORY_SEARCH_INDEX_NAME);
        //将 esKeywordRequest 对象转换为Elasticsearch可以理解的JSON格式，并将其设置为索引请求的源数据。
        indexRequest.source(objectMapper.convertValue(esKeywordRequest, Map.class), XContentType.JSON);
        //执行索引操作，将 indexRequest 中的文档数据发送到Elasticsearch进行索引的。
        client.index(indexRequest, RequestOptions.DEFAULT);
        return Result.success(true);
    }

    /**
     * 获取某一页的匹配视频数据
     */
    @Override
    public Result<List<VideoKeywordSearchResponse>> videoPageKeywordSearch(String keyword, int pageNumber, Integer type) throws JsonProcessingException {
        List<VideoKeywordSearchResponse> videoKeywordSearchResponses = new ArrayList<>();
        SearchRequest searchRequest = new SearchRequest(VIDEO_INDEX_NAME);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构建多字段匹配查询
        searchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, MULTI_QUERY_VIDEO_NAME, MULTI_QUERY_AUTHOR_NAME, MULTI_QUERY_INTRO)
                .type(MultiMatchQueryBuilder.Type.MOST_FIELDS));
        searchSourceBuilder.minScore(1.0f);
        //根据type值决定排序方式
        if (type == 0) {
            //根据分数降序排序
            searchSourceBuilder.sort(ORDER_BY_SCORE, SortOrder.DESC);
        } else if (type == 1) {
            //根据播放量降序排序
            searchSourceBuilder.sort(ORDER_BY_PLAY_COUNT, SortOrder.DESC);
        } else if (type == 2) {
            //根据创建时间降序排序
            searchSourceBuilder.sort(ORDER_BY_CREATE_TIME, SortOrder.DESC);
        } else if (type == 3) {
            //根据弹幕数量降序排序
            searchSourceBuilder.sort(ORDER_BY_DANMAKU_COUNT, SortOrder.DESC);
        } else {
            //根据收藏量降序排序
            searchSourceBuilder.sort(ORDER_BY_COLLECT_COUNT, SortOrder.DESC);
        }
        searchSourceBuilder.fetchSource(true);
        searchRequest.source(searchSourceBuilder);
        SearchResponse response;
        //设置每页大小和偏移量
        searchSourceBuilder.size(size);
        searchSourceBuilder.from((pageNumber - 1) * size);
        try {
            response = client.search(searchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //遍历搜索结果并解析
        for (SearchHit hit : response.getHits().getHits()) {
            videoKeywordSearchResponses.add(objectMapper.readValue(hit.getSourceAsString(), VideoKeywordSearchResponse.class));
        }
        return Result.data(videoKeywordSearchResponses);
    }

    /**
     * 获取某一页的匹配用户数据
     */
    @Override
    public Result<List<UserKeyWordSearchResponse>> userPageKeywordSearch(String keyword, int pageNumber, Integer type, Integer userId) throws JsonProcessingException {
        List<UserKeyWordSearchResponse> userKeyWordSearchResponses = new ArrayList<>();
        SearchRequest searchRequest = new SearchRequest(USER_INDEX_NAME);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, MULTI_QUERY_NICKNAME, MULTI_QUERY_INTRO)
                .type(MultiMatchQueryBuilder.Type.MOST_FIELDS));
        if (type == 0) {
            searchSourceBuilder.sort(ORDER_BY_SCORE, SortOrder.DESC);
        } else if (type == 1) {
            searchSourceBuilder.sort(ORDER_BY_FANS_COUNT, SortOrder.DESC);
        } else {
            searchSourceBuilder.sort(ORDER_BY_FANS_COUNT, SortOrder.ASC);
        }
        searchSourceBuilder.fetchSource(true);
        searchRequest.source(searchSourceBuilder);
        List<Integer> ids = new ArrayList<>();
        SearchResponse response;
        searchSourceBuilder.size(size);
        searchSourceBuilder.from((pageNumber - 1) * size);
        try {
            response = client.search(searchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        for (SearchHit hit : response.getHits().getHits()) {
            ids.add(Integer.valueOf((String) hit.getSourceAsMap().get(INDEX_ID)));
            userKeyWordSearchResponses.add(objectMapper.readValue(hit.getSourceAsString(), UserKeyWordSearchResponse.class));
        }
        Set<Integer> followSet = new HashSet(10);
        LambdaQueryWrapper<Follow> followLambdaQueryWrapper = new LambdaQueryWrapper<>();
        followLambdaQueryWrapper.eq(Follow::getFansId, userId);
        List<Follow> follows = followMapper.selectList(followLambdaQueryWrapper);
        for (Follow follow : follows) {
            followSet.add(follow.getIdolId());
        }
        userKeyWordSearchResponses.forEach(userKeyWordSearchResponse -> {
            if (followSet.contains(userKeyWordSearchResponse.getId())) {
                userKeyWordSearchResponse.setIsFollow(true);
            } else {
                userKeyWordSearchResponse.setIsFollow(false);
            }
        });
        return Result.data(userKeyWordSearchResponses);
    }

    /**
     * 根据给定的搜索词，搜索最可能的关键词
     * 该方法覆盖了默认的搜索行为，专门针对历史搜索记录进行关键词搜索
     *
     * @param searchWord 用户输入的搜索词
     * @return 返回一个包含可能的关键词列表的结果对象
     * @throws IOException 如果搜索过程中发生I/O错误
     */
    @Override
    public Result<List<String>> likelyKeywordSearch(String searchWord) throws IOException {
        // 创建一个针对历史搜索记录索引的搜索请求
        SearchRequest searchRequest = new SearchRequest(HISTORY_SEARCH_INDEX_NAME);
        // 初始化一个列表，用于存储搜索结果中的关键词
        List<String> list = new ArrayList<>();
        // 构建搜索源，包括查询条件等
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 设置查询条件为多字段匹配查询，以适应更复杂的搜索需求
        sourceBuilder.query(QueryBuilders.multiMatchQuery(searchWord, MULTI_QUERY_SEARCH_WORD));
        // 将构建好的搜索源设置到搜索请求中
        searchRequest.source(sourceBuilder);
        // 执行搜索请求，并获取搜索响应
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        // 遍历搜索响应中的每一个搜索命中项
        for (SearchHit searchHit : searchResponse.getHits().getHits()) {
            // 将搜索命中的源数据反序列化为EsKeywordRequest对象
            EsKeywordRequest keywordRequest = objectMapper.readValue(searchHit.getSourceAsString(), EsKeywordRequest.class);
            // 将反序列化得到的搜索词添加到结果列表中
            list.add(keywordRequest.getSearchWord());
        }
        // 将包含关键词的列表包装成Result对象返回
        return Result.data(list);
    }


    /**
     * 推荐视频查询
     * 根据给定的视频ID，查询并推荐类似的视频
     * 该方法使用Elasticsearch的moreLikeThisQuery查询来实现视频推荐，根据视频的内容特征找出相似的视频
     *
     * @param videoId 视频ID，用于查询类似视频的依据
     * @return 返回一个推荐视频的列表，不包括输入的视频ID对应的视频
     * @throws IOException 当搜索操作失败或解析搜索结果时抛出此异常
     */
    @Override
    public List<RecommendVideo> likelyVideoRecommend(String videoId) throws IOException {
        //使用了特定查询query--morelikethisquery来强化对关键字匹配度的查询
        SearchRequest searchRequest = new SearchRequest(VIDEO_INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //创建一个Item 数组，用于构建 moreLikeThisQuery 查询的输入。
        Item[] items = {
                new Item(VIDEO_INDEX_NAME, videoId),
        };
        //按分数降序排序，以确保最相关的视频排在前面
        sourceBuilder.sort(ORDER_BY_SCORE, SortOrder.DESC);
        //构建moreLikeThisQuery查询，使用视频ID作为相似性依据，设置最小词频和最大查询项数以优化查询结果
        sourceBuilder.query(QueryBuilders.moreLikeThisQuery(
                        null,
                        null,
                        items
                ).minTermFreq(1)// 设置最小词频为1，表示至少要出现1次的词才会被考虑
                .maxQueryTerms(12)); // 设置最大查询项数为12，限制生成的查询项数量以提高查询效率
        List<RecommendVideo> list = new ArrayList<>();
        searchRequest.source(sourceBuilder);
        //执行搜索请求并处理响应
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        for (SearchHit searchHit : searchResponse.getHits().getHits()) {
            //将搜索结果反序列化为RecommendVideo对象
            RecommendVideo recommendVideo = objectMapper.readValue(searchHit.getSourceAsString(), RecommendVideo.class);
            //确保不包括输入的视频ID对应的视频在推荐列表中
            if (!recommendVideo.getVideoId().equals(videoId)) {
                list.add(recommendVideo);
            }
        }
        return list;
    }


    /**
     * 创建索引
     * <p>
     * 该方法根据传入的索引请求信息创建一个Elasticsearch索引它首先构建索引的映射结构，
     * 然后使用这些信息来创建实际的索引
     *
     * @param esIndexRequest 包含索引创建所需信息的请求对象，包括索引名称和属性
     * @return 返回一个布尔值表示索引是否创建成功
     */
    @Override
    public Boolean createIndex(EsIndexRequest esIndexRequest) {
        try {
            // 获取索引名称
            String index = esIndexRequest.getIndexName();
            // 获取索引属性
            Map<String, String> map = esIndexRequest.getProperties();
            // 创建JSON内容构建器
            XContentBuilder builder = XContentFactory.jsonBuilder();
            // 开始构建索引的JSON对象
            builder.startObject();
            {
                // 开始索引映射部分
                builder.startObject(INDEX_START_OBJECT_MAPPINGS);
                {
                    // 开始属性部分
                    builder.startObject(INDEX_START_OBJECT_PROPERTIES);
                    {
                        // 遍历传入的属性映射，添加到索引映射中
                        for (Map.Entry<String, String> entry : map.entrySet()) {
                            // 开始单个属性的定义
                            builder.startObject(entry.getKey());
                            {
                                // 定义属性的类型
                                builder.field(INDEX_FIELD_TYPE, entry.getValue());
                            }
                            // 结束单个属性的定义
                            builder.endObject();
                        }
                    }
                    // 结束属性部分
                    builder.endObject();
                }
                // 结束索引映射部分
                builder.endObject();
            }
            // 结束索引的JSON对象构建
            builder.endObject();
            // 创建创建索引请求对象
            CreateIndexRequest request = new CreateIndexRequest(index);
            // 将构建好的索引映射JSON对象作为请求源
            request.source(builder);
            // 执行创建索引请求
            client.indices().create(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            // 打印异常信息
            e.printStackTrace();
        }
        // 返回true表示索引创建流程结束，实际结果可能需要进一步检查
        return true;
    }


    /**
     * 删除索引
     * <p>
     * 此方法用于删除给定名称的索引如果索引不存在，则输出提示信息并返回true
     *
     * @param indexName 索引的名称
     * @return 总是返回true，即使索引不存在
     * @throws IOException 如果与服务器的通信失败
     */
    @Override
    public Boolean deleteIndex(String indexName) throws IOException {
        // 创建一个请求以检查索引是否存在
        GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
        // 检查索引是否存在
        boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
        if (exists) {
            // 如果索引存在，创建一个删除索引的请求
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            // 执行删除操作
            client.indices().delete(request, RequestOptions.DEFAULT);
            // 输出删除成功的信息
            System.out.println("索引删除了");
        } else {
            // 如果索引不存在，输出提示信息
            System.out.println("索引不存在");
        }
        // 返回true表示操作完成
        return true;
    }

}

